package com.wh1200.mybatis.xmlless.resolver

import com.wh1200.kotlin.toCamelCase
import com.wh1200.kotlin.toPascalCase
import com.wh1200.kotlin.toWords
import com.wh1200.mybatis.xmlless.enums.MethodType
import com.wh1200.mybatis.xmlless.enums.QueryType
import com.wh1200.mybatis.xmlless.kotlin.split
import com.wh1200.mybatis.xmlless.model.ChooseCriteria
import com.wh1200.mybatis.xmlless.model.MethodInfo
import com.wh1200.mybatis.xmlless.model.QueryCriteriaGroup
import com.wh1200.mybatis.xmlless.model.SortInfo
import com.wh1200.mybatis.xmlless.model.base.BaseCriteria
import com.wh1200.mybatis.xmlless.model.exception.BuildSQLException
import java.util.*

/**
 * @author 吴昊
 * @date 2024/01/14
 * @version 1.0
 * @since v4.0.0
 */
class NameResolver(
    private val methodInfo: MethodInfo
) {

  private val nameToResolve = getMethodName()
  private val words: Stack<String> = toWordsStack(nameToResolve)

  fun resolve(): QueryResolver.NameResolveResult {
    val queryType = resolveType()
    val properties = resolveProperties()
    val excludeProperties = resolveExcludeProperties()
    val conditions = resolveGroupedConditionList(queryType)
    val sorts = resolveSorts()
    val groupBy = resolveGroupBy()
    val havingConditions = resolveHaving()
    if (words.isNotEmpty()) {
      throw BuildSQLException("无法解析SQL，解析的名称为$nameToResolve")
    }
    return QueryResolver.NameResolveResult(
        queryType, properties, excludeProperties,
        conditions, sorts, groupBy, havingConditions
    )
  }

  private fun getMethodName(): String {
    return if (methodInfo.queryType == null) {
      methodInfo.name.toPascalCase()
    } else {
      ""
    }
  }

  private fun toWordsStack(name: String): Stack<String> {
    val stack = Stack<String>()
    if (name.isBlank()) {
      return stack
    }
    name.toWords().map {
      if (it == "ids") {
        listOf("Id", "In")
      } else {
        listOf(it)
      }
    }.flatten().toMutableList().reversed().forEach {
      stack.push(it)
    }
    return stack
  }

  private fun resolveHaving(): List<BaseCriteria> {
    if (words.isEmpty()) {
      return listOf()
    }
    if (words.last() != "Having") {
      return listOf()
    }
    words.pop()
    val remainWords = takeUntil { false }
    return CriteriaResolver.resolveConditions(remainWords, methodInfo).onEach {
      it.forHaving()
    }
  }

  private fun resolveGroupBy(): List<String> {
    if (words.lastOrNull() != "Group") {
      return listOf()
    }
    words.pop()
    if (words.lastOrNull() != "By") {
      throw BuildSQLException("无法解析分组字段，解析的名称为$nameToResolve")
    }
    words.pop()
    val words = takeUntil {
      words.lastOrNull() == "Having"
    }
    return words.split("And").map { it.joinToString("").toCamelCase() }
  }

  private fun resolveExcludeProperties(): List<String> {
    return methodInfo.excludeProperties.toList() + methodInfo.excludes
  }

  /**
   * 解析要查询或者更新的字段
   */
  private fun resolveProperties(): List<String> {
    // 如果方法指定了要查询或者更新的属性，从方法名称解析的字段无效
    val specificProperties = methodInfo.selectedProperties + methodInfo.includes
    if (words.isEmpty()) {
      return specificProperties
    }
    if (words.last() == "By") {
      return specificProperties
    }
    val propertiesWords = takeUntil {
      (words.size > 1 && words.last() in listOf(
          "Order",
          "Group"
      ) && words[words.size - 2] == "By") || words.last() in listOf("Having", "By")
    }
    if (specificProperties.isNotEmpty()) {
      return specificProperties
    }
    return propertiesWords.split("And").map { it.joinToString("").toCamelCase() }
  }

  private fun resolveGroupedConditionList(
      queryType: MethodType
  ): List<BaseCriteria> {
    val result = mutableListOf<BaseCriteria>()
    if (words.lastOrNull() == "By") {
      words.pop()
      val remainConditionWords = takeUntil {
        (words.size > 1 && words.last() in listOf("Order", "Group") && words[words.size - 2] == "By")
            || words.last() in listOf("Having")
      }
      val conditions = CriteriaResolver.resolveConditions(remainConditionWords, methodInfo)
      if (remainConditionWords == listOf("Id")) {
        assert(conditions.size == 1)
        assert(conditions.first().parameters.size == 1)

        result += QueryCriteriaGroup(
            listOf(
                conditions.first().clone(
                    conditions.first().parameters.map { parameter ->
                      parameter.clone(
                          this.methodInfo.findOptionalParam("id")?.name
                            ?: this.methodInfo.modelMappings.tableInfo.keyProperty
                            ?: "id",
                          isNotNull = true, isNotBlank = true, isNotEmpty = true
                      )
                    },
                )
            ), methodInfo
        )
      } else if (conditions.isNotEmpty()) {
        result += QueryCriteriaGroup(
            conditions.map { QueryCriteriaGroup(listOf(it), methodInfo) },
            methodInfo
        )
      }
    }
    result += methodInfo.conditions.map {
      CriteriaResolver.resolveConditionsFromExpression(it.trim(), methodInfo)
    }
    methodInfo.parameters
        .filter { it.chooseCriteria.whenList.isNotEmpty() }
        .forEach { parameter ->
          result += ChooseCriteria(
              parameter.name,
              listOf(
                  CriteriaResolver.resolveCriteriaParameterFromProperty(
                      parameter.name,
                      parameter.parameter,
                      null
                  )
              ),
              methodInfo
          )
        }
    result += CriteriaResolver.createComplexParameterCondition(methodInfo).map {
      QueryCriteriaGroup(it, methodInfo)
    }
    if (methodInfo.isLogic && queryType.isQueryType) {
      result += methodInfo.createLogicCriteria()
    }
    if (result.size == 1) {
      val q = result.first()
      if (q is QueryCriteriaGroup) {
        return q.criterion
      }
    }
    return result
  }

  private fun resolveType(): MethodType {
    val type = if (methodInfo.queryType != null) {
      getTypeFromAnnotation(methodInfo.queryType)
    } else {
      MethodTypeResolver.resolveTypeFromName(words)
    }
    if (type == MethodType.Delete && methodInfo.isLogic) {
      return MethodType.LogicDelete
    }
    return type ?: error("解析SQL类型失败，解析的名称为$nameToResolve")
  }

  private fun getTypeFromAnnotation(type: QueryType): MethodType {
    return when (type) {
      QueryType.Count          -> MethodType.Count
      QueryType.Delete         -> {
        if (methodInfo.isLogic) {
          MethodType.LogicDelete
        } else {
          MethodType.Delete
        }
      }

      QueryType.Exists         -> MethodType.Count
      QueryType.Insert         -> MethodType.Insert
      QueryType.InsertOrUpdate -> MethodType.InsertOrUpdate
      QueryType.Select         -> MethodType.Select
      QueryType.SelectDistinct -> MethodType.SelectDistinct
      QueryType.Update         -> MethodType.Update
      QueryType.None           -> MethodTypeResolver.resolveTypeFromName(
          toWordsStack(methodInfo.name.toPascalCase())
      ) ?: error("无法解析方法${methodInfo.fullMethodName}的SQL类型, 请在@XmllessMethod注解中指定有效的type属性")
    }
  }

  private fun resolveSorts(): List<SortInfo> {
    val sorts = mutableListOf<SortInfo>()
    methodInfo.sorts.forEach {
      sorts.add(QueryResolver.resolveSortFromExpression(it, methodInfo))
    }
    if (words.isNotEmpty() && words.last() == "Order") {
      words.pop()
      if (words.pop() != "By") {
        throw BuildSQLException("无法解析排序字段，解析的名称为$nameToResolve")
      }
      val remainConditionWords = takeUntil {
        (words.size > 1 && words.last() in listOf("Group") && words[words.size - 2] == "By")
            || words.last() in listOf("Having")
      }
      val orderByString = remainConditionWords.joinToString("")
      sorts.addAll(QueryResolver.resolveSortFromExpression(orderByString))
    }
    return sorts
  }

  private fun takeUntil(until: () -> Boolean): List<String> {
    val remainConditionWords = mutableListOf<String>()
    while (words.isNotEmpty() && !until()) {
      remainConditionWords.add(words.pop())
    }
    return remainConditionWords.toList()
  }

}
